Add Apertus tool parser - #1663
Open
gabriben wants to merge 1 commit into
Open
Conversation
Apertus emits tool calls as an array of single key objects wrapped in
<|tools_prefix|> ... <|tools_suffix|>, e.g.
<|tools_prefix|>[{"get_weather": {"location": "London"}}]<|tools_suffix|>
Adds a parser for that format and infers it from the chat template, so
Apertus models work without an explicit tool_parser_type.
Also fixes reasoning detection. Apertus delimits deliberation with
<|inner_prefix|> ... <|inner_suffix|>, but carries unused <think>/</think>
tokens in its vocabulary, so _infer_thinking matched those instead and the
reasoning state was never reachable — deliberation was returned as content
rather than reasoning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Apertus emits tool calls as an array of single key objects wrapped in
<|tools_prefix|>…<|tools_suffix|>:This adds a parser for that format and infers it from the chat template, so
Apertus models work without an explicit
tool_parser_typeintokenizer_config.json. Before this,_infer_tool_parserreturnedNoneforApertus,
has_tool_callingstayedFalse, and the server rejected any requestcarrying
tools.It also fixes reasoning detection for the same models. Apertus delimits
deliberation with
<|inner_prefix|>…<|inner_suffix|>, but it also carriesunused
<think>/</think>tokens in its vocabulary (ids 69/70). Since_infer_thinkingscans the vocab, it matched the unused pair, so the reasoningstate was never reachable and deliberation came back as
contentinstead ofreasoning. The Apertus pair is now checked first.Both changes are in one PR because they enable the same models and touch the
same file, following #810, which added the LongCat tool parser and its
THINK_TOKENSentry together.Provenance
Derived from vLLM.
parse_tool_callis adapted from vLLM's Apache-2.0licensed
apertus_tool_parser.py— specifically its non-streaming
extract_tool_calls, whose structure it keeps:wrapping a non-list payload, skipping entries that aren't non-empty objects, and
next(iter(obj.items()))to split the single key into name and arguments.Reduced to the batch path, since mlx-lm buffers the whole tool region and parses
it once, so vLLM's streaming diff machinery has no analogue here. The header
follows
kimi_k2.py, which is likewise a "Modified from" vLLM port. Flagging theApache-2.0 → MIT direction explicitly in case you'd prefer different attribution.
Informed by SGLang, but not derived from it. No code is taken from SGLang's
apertus2509_detector.py;its equivalent helper is written differently. What it contributed is one
behavioural decision — normalizing null arguments to
{}, so clients neverreceive
"arguments": "null"— and corroboration of two facts: the wire format,and the reasoning marker names, which match its
Apertus2509Detectorinreasoning_parser.py. Both facts were also verified directly against Apertus'chat template and vocabulary rather than taken on trust.
Deliberately not carried over from either: partial-JSON recovery for calls
truncated mid-generation (vLLM's
test_incomplete_tool_call). No mlx-lm parserdoes this, and since tool regions are parsed once rather than incrementally, a
truncated call raises and is logged by the server instead.
Note on
<|tools_suffix|>Worth flagging for future reference: Apertus lists
<|tools_suffix|>ineos_token_id([2, 68, 72]), so its tool-call end marker is also an EOStoken. This works on current
mainbecause stop-sequence matching(
StopSequenceMatcher) is separate from the text state machine. It did notwork in 0.31.3, where EOS ids were folded into the tool state's transition trie
as stop edges — the duplicate
(72,)overwrote the tool-end transition, so toolcalls were parsed into a buffer and then silently discarded (HTTP 200, empty
assistant message). No change is needed for this, but it's a real constraint on
that part of the design.
Testing
tests/test_tool_parsing.py::test_apertus— single and multiple calls, nestedarguments, null arguments, bare object instead of an array, unparseable
entries skipped, and
ValueErroron nothing-parseable and on a call truncatedmid generation.
tests/test_tokenizers.py::test_thinking_marker_precedence— locks in thatApertus' markers win over the unused
<think>pair. Uses a stub vocab so itneeds no network, following
test_find_token's precedent of testing internals.tests/test_tool_parsing.py,tests/test_server.py(25 tests), and thetargeted tokenizer tests pass locally.
pre-commit(black, isort) clean.mlx_lm.serverwithtokimoa/apertus-v1.5-8b-mlx-8bit:tool_callsemitted withfinish_reason: "tool_calls"on both the batch and streaming paths, and a1032-char
reasoningfield correctly separated fromcontentwithenable_thinking: true.swiss-ai/Apertus-v1.5-8Band thequantized
tokimoa/apertus-v1.5-8b-mlx-8bit, neither of which carries atool_parser_typekey.Not run locally: the full
tests/test_tokenizers.pysuite, which downloadsseveral multi-GB models.
Follow-up, deliberately not in this PR
_infer_thinkingresolves markers by scanning the vocabulary, which makes itorder-dependent for any model shipping unused marker tokens — Apertus is the
first case, but it won't be the last, and the failure is silent (reasoning
appears as content). Keying off the chat template, as
_infer_tool_parserdoes,would be more robust. Worth raising as an issue rather than expanding this PR.